package com.free4inno.knowledgems.service;

import com.alibaba.fastjson.JSONObject;
import com.free4inno.knowledgems.constants.UserConstants;
import com.free4inno.knowledgems.dao.GroupInfoDAO;
import com.free4inno.knowledgems.dao.ResourceDAO;
import com.free4inno.knowledgems.dao.ResourceESDAO;
import com.free4inno.knowledgems.dao.UserGroupDAO;
import com.free4inno.knowledgems.domain.GroupInfo;
import com.free4inno.knowledgems.domain.Resource;
import com.free4inno.knowledgems.domain.ResourceES;
import com.free4inno.knowledgems.domain.UserGroup;
import com.free4inno.knowledgems.utils.WriteEsResourceHttpUtils;
import com.free4inno.knowledgems.utils.HighlightResultMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.ScoreSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.*;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.elasticsearch.index.query.QueryBuilders.*;

/**
 * Author HUYUZHU/LIYUNZE/LIUXINYUAN.
 * Date 2021/3/27 13:42.
 */

@Slf4j
@Service
@EnableScheduling
public class ResourceEsService {

    //ES搜索语句builder
    BoolQueryBuilder reBuilder = new BoolQueryBuilder();

    //ES资源更新线程池
    ExecutorService executorService = Executors.newCachedThreadPool();

    @Autowired
    private ResourceESDAO resourceESDao;

    @Autowired
    private ResourceDAO resourceDao;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Autowired
    private WriteEsResourceHttpUtils writeEsResourceHttpUtils;

    @Autowired
    private UserGroupDAO userGroupDao;

    @Autowired
    private GroupInfoDAO groupInfoDao;

    @Value("${spring.data.elasticsearch.cluster-nodes}")
    private String esNodes;

    @Value("${attatchment.download.url}")
    private String downloadUrl;

    @Value("${es.index.resource.name}")
    private String indexName;

    private enum PermissionStatus {
        PUBLIC, PRIVATE;

        public String toString() {
            switch (this) {
                case PUBLIC:
                    return "1";
                case PRIVATE:
                    return "0";
            }
            return super.toString();
        }
    }

    // 复制索引
    public Boolean cloneIndex(String sourceIndex, String destIndex) {
        log.info(this.getClass().getName() + "----in----" + "复制索引" + "----");
        /* 使用 okhttp3 直接通过 http 操作 es */
        // set host
        String esHost = esNodes.substring(0, esNodes.indexOf(":"));
        String url = "http://" + esHost + ":9200/_reindex";
        // set body json
        Map<String, Object> jsonObject = new HashMap<>();
        jsonObject.put("source", new HashMap<String, Object>() {{
            put("index", sourceIndex);
        }});
        jsonObject.put("dest", new HashMap<String, Object>() {{
            put("index", destIndex);
        }});
        JSONObject json = new JSONObject(jsonObject);
        MediaType JSON = MediaType.parse("application/json; charset=utf-8");
        // http request

        RequestBody body = RequestBody.create(JSON, json.toString());
        Request request = new Request.Builder().url(url).post(body).build();
        OkHttpClient client = new OkHttpClient();
        try {
            Response response = client.newCall(request).execute();
            if (response.code() == 200) {
                log.info(this.getClass().getName() + "----out----" + "复制成功" + "----");
                response.close();
                return true;
            } else {
                log.info(this.getClass().getName() + "----out----" + "复制失败" + "----");
                response.close();
                return false;
            }

        } catch (IOException e) {
            e.printStackTrace();
            log.info(this.getClass().getName() + "----out----" + "复制异常" + "----");
            return false;
        }
    }

    // 重建索引
    public boolean rebuildIndex(String name) {
        log.info(this.getClass().getName() + "----in----" + "重建索引" + "----");
        try {
            // 1. delete all index
            resourceESDao.deleteAll();
            // 2. get all resource in database
            List<Resource> resourcesList = resourceDao.findAll();
            // 3. add every resource into index
            for (Resource resource : resourcesList) {
                ResourceES resourceES = new ResourceES();
                resourceESDao.save(writeEsResourceHttpUtils.writeResourceToESHTTP(resource, resourceES));
            }
            log.info(this.getClass().getName() + "----out----" + "重建索引成功" + "----");
            return true;
        } catch (Exception e) {
            log.info(this.getClass().getName() + "----out----" + "重建索引失败" + "----");
            return false;
        }
    }

    // 删除索引
    public boolean deleteIndex(String name) {
        log.info(this.getClass().getName() + "----in----" + "删除索引" + "----");
        try {
            elasticsearchTemplate.deleteIndex(name);
            log.info(this.getClass().getName() + "----out----" + "删除索引成功" + "----");
            return true;
        } catch (Exception e) {
            log.info(this.getClass().getName() + "----out----" + "删除索引失败" + "----");
            return false;
        }
    }

    // 在ES中更新新建的资源
    public void addResourceES(int id) {
        log.info(this.getClass().getName() + "----in----" + "在ES中更新新建的资源" + "----");
        Resource resource = resourceDao.findResourceById(id);
        ResourceES resourceES = new ResourceES();
        //使用线程池完成资源更新，避免用户等待时间过长
        executorService.execute(() -> writeEsResourceHttpUtils.writeResourceToESHTTP(resource, resourceES));
        log.info(this.getClass().getName() + "----out----" + "在ES中更新新建资源完毕" + "----");
    }

    // 在ES中更新编辑过的资源
    public void updateResourceES(int id) {
        log.info(this.getClass().getName() + "----in----" + "在ES中更新编辑过的资源" + "----");
        Resource resource = resourceDao.findResourceById(id);
        ResourceES resourceES = new ResourceES();
        //使用线程池完成资源更新，避免用户等待时间过长
        executorService.execute(() -> writeEsResourceHttpUtils.writeResourceToESHTTP(resource, resourceES));
        log.info(this.getClass().getName() + "----out----" + "在ES中更新编辑资源完毕" + "----");
    }

     /* 通过多线程来实现资源更新 不再采用周期性更新的方法
    public void updateResourceES() {
        log.info(this.getClass().getName() + "----in----" + "更新ES资源" + "----");
        // 获取更新周期，为了后续进行资源类型判断判断
        int period = Integer.parseInt(getPeriod());
        // log.info("周期为" + period + "秒");
        List<Resource> resourcesList = resourceDao.findAll();
        // 获取现在的时间
        LocalDateTime nowTime = LocalDateTime.now();
        // 获取上次更新时间（现在的时间减去更新周期）
        LocalDateTime lastUpdate = nowTime.minusSeconds(period);
        // 获取数据库资源中的编辑时间和新建时间
        for (Resource resource : resourcesList) {
            LocalDateTime createTime = resource.getCreateTime().toLocalDateTime();
            LocalDateTime editTime = resource.getEditTime().toLocalDateTime();
            // 判断条件为：编辑时间在上次更新之后，创建时间在上次更新之前，即为新编辑资源
            int id = resource.getId();
            if (createTime.isBefore(lastUpdate) && editTime.isAfter(lastUpdate)) {
                log.info("编辑时间在上次更新之后，创建时间在上次更新之前，即为新编辑资源" + "----");
                updateResourceES(id);
            }
            // 判断条件为：创建时间在上次更新之后,即为新增资源
            if (createTime.isAfter(lastUpdate)) {
                log.info("创建时间在上次更新之后,即为新增资源" + "----");
                addResourceES(id);
            }
        }
        log.info(this.getClass().getName() + "----out----" + "更新ES资源完毕" + "----");
    }
    */

    // 在ES中删除资源
    public void deleteResourceES(int id) {
        log.info(this.getClass().getName() + "----in----" + "在ES中删除资源" + "----");
        try {
            ResourceES resourceES = new ResourceES();
            resourceES.setId(id);
            resourceESDao.delete(resourceES);
            //deleteAttachById(id); //因将父子文档改为嵌套文档，因此不再需要删除附件索引
            log.info(this.getClass().getName() + "----out----" + "在ES中删除资源完毕" + "----");
        } catch (Exception e) {
            e.printStackTrace();
            log.error(this.getClass().getName() + "----" + "在ES中删除资源" + "----failure----" + "删除失败" + "----");
        }
    }

    // ES搜索
    public Page<ResourceES> searchResourceES(int page, int size, String query, ArrayList<String> groupIds, ArrayList<String> labelIds, int searchTimes, boolean login, String userId) {
        /*
        System.out.println("page");
        System.out.println(page);
        System.out.println("size");
        System.out.println(size);
        System.out.println("query");
        System.out.println(query);
        System.out.println("groupIds");
        System.out.println(groupIds);
        System.out.println("labelIds");
        System.out.println(labelIds);
        System.out.println("searchTimes");
        System.out.println(searchTimes);
        System.out.println("userId");
        System.out.println(userId);
        System.out.println("login");
        System.out.println(login);
         */

        log.info(this.getClass().getName() + "----in----" + "ES搜索" + "----");
        String search_query = query;
        Page<ResourceES> resourcePage;
        if (searchTimes == 1) {
            reBuilder = new BoolQueryBuilder();
        }
        // never seen enter this branch
        if ((query == null || query.isEmpty()) && (groupIds == null || groupIds.isEmpty()) && (labelIds == null || labelIds.isEmpty())) {
            Sort sort = Sort.by(Sort.Direction.DESC, "create_time");
            Pageable pageable = PageRequest.of(page, size, sort);
            if (login) {
                log.info("用户已登录，查询全部资源" + "----");
                resourcePage = resourceESDao.findAll(pageable);
            } else {
                log.info("用户未登录，查询公开资源" + "----");
                resourcePage = resourceESDao.findResourceESByPermissionId("1", pageable); //未登录只能查询公开资源
            }
        } else {
            Pageable pageable = PageRequest.of(page, size);
            /*
             * BUG(此bug继承自265行附近的bug)
             * 此处存在一个奇怪的bug，只单独搜索group_id=89即自由之翼时，将无法搜索到任何内容！但搭配其他搜索条件时搜索正常，且单独搜索其他群组或标签搜索正常，检查索引一切正常。
             * 尝试了多种方案调查均无果，推测可能由于版本问题导致。
             * 只能暂时用这种方式，当只有一个搜索条件时重复手动添加一个字符串，不让单独的一个标签或群组作为搜索条件，即可规避该bug。
            if ((query == null || query.isEmpty()) && (groupIds != null) && (!groupIds.isEmpty()) && (labelIds == null || labelIds.isEmpty())) {
                query = "的";
            }
             */
            // generate HighlightBuilder & InnerHitBuilder for attachment & comment
            HighlightBuilder highlightBuilderAttachText = new HighlightBuilder().field("attachment.attach_text");
            HighlightBuilder highlightBuilderComment = new HighlightBuilder().field("comment.comment");
            InnerHitBuilder innerHitBuilderAttachText = new InnerHitBuilder().setHighlightBuilder(highlightBuilderAttachText);
            InnerHitBuilder innerHitBuilderComment = new InnerHitBuilder().setHighlightBuilder(highlightBuilderComment);

            NestedQueryBuilder nestedQueryAttach = new NestedQueryBuilder("attachment", new MatchQueryBuilder("attachment.attach_text", query), ScoreMode.Total).boost(0.2f).innerHit(innerHitBuilderAttachText);
            NestedQueryBuilder nestedQueryComment = new NestedQueryBuilder("comment", new MatchQueryBuilder("comment.comment", query), ScoreMode.Total).boost(1.2f).innerHit(innerHitBuilderComment);
            MatchQueryBuilder matchQueryBuilderTitle = new MatchQueryBuilder("title", query).boost(1.4f);
            MatchQueryBuilder matchQueryBuilderText = new MatchQueryBuilder("text", query).boost(1.2f);
            reBuilder = reBuilder.should(matchQueryBuilderTitle).should(matchQueryBuilderText).should(nestedQueryAttach).should(nestedQueryComment);

            //进行搜索权限限制：仅搜索无群组和自己所在群组的资源
            // 1. 获取用户不属于的所有群组id
            // 1.1 generate groupList<String>: (all - user.groupIds)
            ArrayList<String> mustNotGroups = new ArrayList<>();
            // 1.2 add all
            List<GroupInfo> allGroups = groupInfoDao.findAll();
            for (GroupInfo groups : allGroups) {
                mustNotGroups.add(groups.getId().toString());
            }
            // 1.3 remove user.groupIds
            List<UserGroup> userGroups = userGroupDao.findByUserId(Integer.parseInt(userId));
            for (UserGroup groups : userGroups) {
                mustNotGroups.remove(groups.getGroupId().toString());
            }
            // 1.4 set not must: 不搜索不在群组的内容
            for (String groupInfo : mustNotGroups) {
                reBuilder.mustNot(termQuery("group_id", groupInfo));
            }

//            ArrayList<String> groupid_list
            //2.对搜索进行限制：即仅搜索无群组和自己所在群组的资源，也即搜索不在自己不在的群组里的资源
//            for (UserGroup groups : userGroups) {
//                reBuilder = reBuilder.must(termQuery("group_id", groups.getGroupId()));
//            }
//            reBuilder = reBuilder.mustNot(termQuery("group_id", "1"));
            if ((groupIds != null) && (!groupIds.isEmpty())) {
                for (String groupId : groupIds) {
                    reBuilder = reBuilder.must(termQuery("group_id", groupId));
                }
                // 有必要么？
                if (groupIds.size() == 1) {
                    reBuilder = reBuilder.must(termQuery("group_id", groupIds.get(0)));
                }
            }
            if ((labelIds != null) && (!labelIds.isEmpty())) {
                for (String labelId : labelIds) {
                    reBuilder = reBuilder.must(termQuery("label_id", labelId));
                }
                // 有必要么？
                if (labelIds.size() == 1) {
                    reBuilder = reBuilder.must(termQuery("label_id", labelIds.get(0)));
                }
            }
            // 按照命中率倒序
            ScoreSortBuilder scoreSortBuilder = new ScoreSortBuilder();
            // 按照时间倒序
            FieldSortBuilder timeSortBuilder = new FieldSortBuilder("create_time");
            timeSortBuilder.order(SortOrder.DESC);
            // 高亮搜索设置
            HighlightBuilder.Field highlightBuilderFieldTitle = new HighlightBuilder.Field("title");
            HighlightBuilder.Field highlightBuilderFieldText = new HighlightBuilder.Field("text");
            // 构造SearchQuery
            SearchQuery searchQuery;
            if (login) {
                searchQuery = new NativeSearchQueryBuilder()
                        .withQuery(reBuilder).withPageable(pageable)
                        .withSort(scoreSortBuilder).withSort(timeSortBuilder)
                        .withMinScore(0.2f)
                        .withHighlightFields(highlightBuilderFieldTitle, highlightBuilderFieldText)
                        .build();
            } else {
                searchQuery = new NativeSearchQueryBuilder()
                        .withFilter(termsQuery("permissionId", "1"))
                        .withPageable(pageable).withSort(scoreSortBuilder)
                        .withSort(timeSortBuilder).withMinScore(0.2f)
                        .withHighlightFields(highlightBuilderFieldTitle, highlightBuilderFieldText)
                        .build();

            }//用户未登录，只能查询公开信息
            //System.out.println(searchQuery.getQuery());
            resourcePage = elasticsearchTemplate.queryForPage(searchQuery, ResourceES.class, new HighlightResultMapper());
            //resourcePage.forEach(item -> System.out.println(item.getTitle()));
        }
        log.info(this.getClass().getName() + "----out----" + "返回ES查询到的resourcePage" + "----");
        return resourcePage;
    }

    // 获取时间周期
    public String getPeriod() {
        log.info(this.getClass().getName() + "----in----" + "获取ES更新周期" + "----");
//        String cron = cronDao.findByCronId(1).getCron();
//        int length = cron.length();
//        return cron.substring(2, length).replaceAll("[*]", "").replaceAll("[?]", "").trim();
        //TODO:暂时写死，后续考虑计算方法
        log.info(this.getClass().getName() + "----out----" + "返回180(后端写死)" + "----");
        return "180";
    }
}
